03 — 第一个 Three.js 项目
介绍
话不多说,现在是时候开始编码,创建我们的第一个 Three.js 场景了。
如果你已经熟悉如今的前端开发工具,比如 Node.js 和 Vite,你大概可以跳过本节课的大部分内容。
否则,请跟着我们一起走,别担心,我会把一切都详细讲清楚。
本地服务器和构建工具
为了运行我们即将诞生的 Three.js 网站,我们需要运行一个本地服务器。
我们可以把 HTML 文件创建在电脑上的某个地方,然后双击打开,但出于安全原因,浏览器会限制这种打开方式的网站功能。这样一来,我们无法加载 Three.js、模型、纹理等等。
解决这个问题最简单的办法,就是使用"构建工具"(build tool)或"打包器"(bundler)。
构建工具的现状
如今可用的构建工具有很多,你可能听说过其中一些,比如 Webpack、Vite、Gulp、Parcel 等等。
它们各有各的功能和优缺点,但在接下来的课程中,我们会使用一个非常特定的工具。
如今,最流行的构建工具是 Webpack。它被广泛使用,社区庞大,而且用它你能做很多事情。但虽然 Webpack 是最流行的,却不是最受好评的。
事实上,这些天最受好评的构建工具是 Vite(法语中"快速"的意思,发音为 /vit/,类似"veet")。它安装更快、运行更快,也更不容易出 bug。归根结底,开发者体验要好得多。
最初,所有 Three.js Journey 的练习都在 Webpack 上运行,下面有些课程也是那样录制的。但现在,练习都跑在 Vite 上了。别担心,我把 Vite 配置成让文件和 Vite 的行为看起来和 Webpack 配置非常相似,你可能在看视频时甚至察觉不到差异。
Vite
这堂课不是讲 Vite 的,但因为我忍不住,我还是会解释一下它的几个方面。
正如前面提到的,Vite 是一个构建工具。思路是我们要编写 HTML/CSS/JS 这样的 Web 代码,然后由 Vite 构建出最终的网站。它还会做一堆事情,比如优化、缓存破坏(cache breaking)、源码映射、运行本地服务器等等。
虽然 Vite 能满足最基本的需求,但我们还可以添加插件来处理更多功能,比如各种语言或特殊文件。实际上,我们会在课程后面添加插件,以便能处理 GLSL 文件来创建自定义 shader,同时也能运行 React。
Vite 由 Vue.js 的创造者 Evan You 创建,它有数百位开发者持续维护,而且人气正高。
第一个 Vite 项目
你可能已经注意到这节课附带了一个 starter(起始)文件。在后面的课程里,你还会看到 final(最终)文件。
starter 文件是你跟随练习开始时需要用到的,而 final 文件是练习的最终结果,以备你需要参考。但这次我们不会马上用 starter 文件,我会先演示如何从零开始一个项目。到后面阶段,我们会用到那个文件。
终端
我们会在终端中运行一些命令。我们可以使用 MacOS 的 Terminal(终端)或 Windows 的 Command Prompt(命令提示符),但我实际上推荐你使用 VSCode 内置的终端(如果你像我一样用 VSCode 的话)。启动 VSCode,按 CMD + J(MacOS)或 CTRL + J(Windows)打开内置终端。
如果这样做,无论你用什么操作系统,下面的命令都是一样的。
终端的工作方式有点像文件资源管理器,你需要在正确的文件夹里才能运行命令。
你可以用 cd 后面跟文件夹名来进入其中。你也可以输入 cd (末尾带一个空格),然后直接把文件夹拖放进去。你可以用 pwd 测试当前所在位置,用 ls 列出当前文件夹中的文件。
故障排除
我们会执行一堆命令和操作。如果你遇到任何问题,在课程末尾附近有一节我列出了经典问题的章节。如果仍然无法解决,作为 Three.js Journey 的成员,你可以访问一个私密的 Discord 服务器。使用顶部菜单进入 Discord 页面,在那里分享你的问题。我们会帮你解决。
Node.js
要运行 Vite,你需要在电脑上安装 Node.js。
Node.js 让你能在浏览器之外、在电脑上运行 JavaScript。它非常适合运行像 Vite 这样各种工具,它已经存在很多年了,而且非常流行。
如果你不确定 Node.js 是否已经安装,或者安装的是什么版本,在终端中运行 node -v 即可。
如果输出提示 node 命令无法识别,那就说明没有安装。
如果已安装,输出会包含当前版本号。请确保它更新到最新版本。Vite 目前可运行在 14.18 及以上版本,但我建议你始终安装 Node.js 的 LTS(长期支持)版本。
要安装或更新 Node.js,请访问 https://nodejs.org/en/,下载"LTS"并按默认设置安装。
关闭你的 Terminal(MacOS)或 Command Prompt(Windows),重新打开,再次运行 node -v 检查版本。
创建一个 Node.js 项目
现在我们已经有了 Node.js,也知道了如何使用终端,是时候创建我们的 Node.js 项目了。
创建一个用来存放你网站的文件夹(尽量保持条理,后面还有一大堆课程要学)。
你现在可以做的,是在 VSCode 中打开这个文件夹(如果你用 VSCode 的话)。这有两个主要原因会派上用场:
- VSCode 终端会直接定位在该文件夹里。
- 我们可以在侧边栏更方便地管理文件,而不会像操作系统资源管理器那样隐藏某些文件。
现在打开应该已经定位在该文件夹的终端,运行 npm init -y。
当我们安装 Node.js 时,会自动安装 NPM。NPM 是 Node Package Manager 的缩写,是一个依赖管理器,会帮我们拉取所需的包,比如 Three.js、Vite,以及课程中要用到的各种库。此外,NPM 附带一个名为 npm 的命令行工具,我们可以从终端使用它。
运行 npm init -y 后,NPM 会创建一个 package.json,里面包含运行一个 Node.js 项目所需的最基本信息。
添加依赖
在这节课中,我们会用到两个依赖:Vite 和 Three.js。
如果你好奇,可以在 NPM 网站上找到它们:
目前,我们只关注 Vite,稍后再添加 Three.js。要向 Node.js 项目添加依赖,在终端中运行 npm install vite。
你应该会注意到三件事。
- 创建了一个
node_modules/文件夹。这个文件夹存放项目依赖,你永远不应该修改里面的东西。除了以vite/文件夹的形式包含vite依赖外,它还包含了vite自身所依赖的其他依赖。 package.json已被更新,现在包含一个写在"dependencies"属性里的依赖数组,数值对应版本号,并带有一定的容错级别。- 生成了一个
package-lock.json,里面记录着项目依赖信息,以及已安装的精确版本,没有容错。
这 2 个文件和那个文件夹实现了项目共享。如果你想把自己漂亮的 Three.js 网站分享给另一位开发者,你会想移除 node_modules 文件夹,然后分享项目其余部分。对方拿到你的项目后,运行一条命令,就会根据 package.json 中写的内容自动重建 node_modules 文件夹。
不知道怎么做?别担心,我们一会儿就会复习那条命令。注意 package-lock.json 是可选的,但如果项目里有它,对方得到的依赖就会和你完全一样,没有任何容错偏差。
基础网站
我们有了 Node.js 项目,有了作为依赖添加进来的 Vite。现在,我们需要创建一个基础网站。
仍在项目文件夹中,创建一个 index.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>03 - First Three.js Project</title>
</head>
<body>
<h1>Soon to be a Three.js website</h1>
</body>
</html> 不要直接打开这个文件!那样就违背了我们正在用 Vite 做的事。
在 package.json 中,把 "scripts" 部分替换成下面这样:
{
// ...
"scripts": {
"dev": "vite",
"build": "vite build"
},
// ...
} 有了这两个"scripts",我们现在可以从终端运行 dev 和 build 命令了,这两条命令分别通过 node_modules/ 文件夹中的 vite/ 依赖触发 vite 和 vite build。
要运行 dev 脚本,在终端中输入 npm run dev。
Vite 应该会显示一个类似 http://localhost:5173/ 的 URL。把它复制到浏览器中打开,就像打开任何网站一样。
恭喜,你的 Vite 网站已经跑起来了。
你的终端看起来可能像卡住了,但其实它现在正在运行服务器,停止这个脚本就会导致服务器关闭,我们一会儿就会这么做。
build 命令会输出网站的最终版本。不过这要等到后面的课程,我们会学习如何让网站上线。
添加 JavaScript
这很棒,但现在我们想在漂亮的网站上执行一些 JavaScript。
仍在项目文件夹中,创建一个 script.js 文件:
console.log('JavaScript is working') 在 index.html 中,把下面的 <script> 加到 <body> 的末尾:
<!-- ... -->
<body>
<h1>Soon to be a Three.js website</h1>
<script type="module" src="./script.js"></script>
</body>
</html> 别忘了 type="module"。
打开你的 Developer Tools(开发者工具,或简称 DevTools),你应该能在 Console(控制台)里看到 JavaScript is working。
你可能已经注意到,你甚至都不需要刷新页面。Vite 检测到了改动,会自动为你刷新页面。
Three.js
就像我们当初把 Vite 加进项目一样,现在需要把 Three.js 加进去。
添加依赖
这个依赖在 NPM 上的包名是 three。
我们需要运行 npm install three,但当前终端正忙于运行 Vite 服务器。
你有两个选择:
- 打开一个新终端并运行命令,同时确保你在同一个文件夹里。
- 关闭服务器,运行命令,然后重新启动服务器。
我们选择关闭服务器。
按 CTRL + C 停止服务器。在 Windows 系统上你可能需要按多次,或者用字母 o 确认。
现在运行 npm install three:
three/ 文件夹应该已经被添加到了 node_modules/ 文件夹中,package.json 和 package-lock.json 现在也都包含了对它的引用。
用 npm run dev 重新启动服务器:
导入 Three.js
回到 script.js,我们可以通过这样写来从 three 依赖中导入 Three.js:
import * as THREE from 'three' 这会把 Three.js 的所有核心类从 three 依赖导入到一个名为 THREE 的变量中。
这种导入依赖的方式叫做"ES modules"。我们不会深入这个主题,因为它更多属于 JavaScript 基础,而不是 Three.js。将来,在课程中需要的时候,我们还会介绍更多例子。
使用 Three.js
在我们的 script.js 文件里,现在我们有了一个名为 THREE 的变量可用。注意要始终用大写字母写它。
如果你对这个变量执行 console.log(),你会看到里面内容相当丰富:
import * as THREE from 'three'
console.log(THREE)
THREE 变量包含了一个经典 Three.js 项目中你可能需要的大部分类和属性。遗憾的是,并非所有类都在这个变量里,不过我们以后会知道如何访问它们。
要使用其中一个类,你需要实例化它。例如,如果你想创建一个场景,你会写 const scene = new THREE.Scene()。如果你想创建一个球体几何体,你需要写 const sphereGeometry = new THREE.SphereGeometry(1.5, 32, 32)——这些我们以后会深入讲解。
第一个场景
是时候创建我们的场景,在屏幕上产生点什么了。
要起步,我们需要 4 个要素:
- 一个用于容纳物体的场景
- 一些物体
- 一个相机
- 一个渲染器
场景
场景就像一个容器。你把你的物体、模型、粒子、灯光等等放进去,然后在某个时刻,你让 Three.js 渲染这个场景。
要创建场景,使用 Scene 类:
// Scene
const scene = new THREE.Scene()
物体
物体可以有很多种。你可以有基础几何体、导入的模型、粒子、灯光等等。
我们先从一个简单的红色立方体开始。
要创建那个红色立方体,我们需要创建一种名为 Mesh(网格)的物体。一个 Mesh 是几何体(形状)和材质(外观)的组合。
几何体和材质都有很多种,但我们现在先保持简单,创建一个 BoxGeometry 和一个 MeshBasicMaterial。
要创建几何体,我们使用 BoxGeometry 类,前 3 个参数对应盒子的尺寸。
// Object
const geometry = new THREE.BoxGeometry(1, 1, 1) 要创建材质,我们使用 MeshBasicMaterial 类,带一个参数:一个包含所有选项的对象 {}。我们只需要指定它的 color(颜色)属性。
在 Three.js 中指定颜色有很多方式。你可以传一个 JS 十六进制数 0xff0000,你可以传一个字符串形式的十六进制 '#ff0000',你可以用像 'red' 这样的颜色名称,或者你可以在 Color 类下传一个实例——关于这个我们后面会多讲一些。
// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }) 要创建最终的 mesh,我们使用 Mesh 类,并把 geometry 和 material 作为参数传进去。
// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material) 现在你可以用 add(...) 方法把 mesh 添加到场景中:
scene.add(mesh) 如果你不把物体添加到场景里,你就看不到它。
相机
相机是不可见的。它更像是一个理论上的观察视角。当我们渲染场景时,会从这个相机的视角进行渲染。
你可以像电影片场那样拥有多台相机,并且可以随心所欲地在这些相机之间切换。通常我们只用一台相机。
相机有不同类型,我们会在以后的课程中讨论。目前,我们只需要一个能处理透视的相机(让近处的物体看起来比远处的物体更突出)。
要创建相机,我们使用 PerspectiveCamera 类。
我们需要提供两个至关重要的参数。
视场角(field of view)
视场角就是你的视野角度有多大。如果你用非常大的角度,你就能同时看到所有方向,但会有很大的畸变,因为结果要画在一个小矩形上。如果你用小角度,东西看起来就像被放大了。
视场角(或 fov)用度表示,对应垂直方向的视野角度。此次练习中我们采用 75 度的角度。
这里有一段视频讲解视场角变化看起来是什么样:
纵横比(aspect ratio)
大多数情况下,纵横比就是 canvas 的宽度除以高度。 我们目前还没有指定任何宽度或高度,但以后会有需要。在此期间,我们会创建一个带临时值的对象,之后可以复用它。
别忘了把相机添加到场景中。即使不把相机添加到场景,一切也应该能工作,但以后可能会导致一些 bug:
// Sizes
const sizes = {
width: 800,
height: 600
}
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
scene.add(camera)
渲染器
渲染器的工作就是做渲染。想不到吧!
我们只需请渲染器从相机的视角渲染我们的场景,结果会被绘制到一个 canvas 里。你可以自己创建 canvas,也可以让渲染器生成它,然后添加到你的页面。本次练习中,我们会把 canvas 加到 HTML 里,再传给它给渲染器。
在 index.html 中,把 <h1> 替换为 <canvas> 元素,放在加载脚本的前面,并给它一个 class:
<canvas class="webgl"></canvas> 要创建渲染器,我们使用 WebGLRenderer 类,带一个参数:一个包含所有选项的对象 {}。我们需要指定 canvas 属性,对应我们添加到页面的那个 <canvas>。
在代码开头创建一个 canvas 变量,然后用 document.querySelector(...) 抓取我们在 HTML 中创建的元素并存储进去。
最好把 canvas 赋给一个变量,因为在接下来的课程中我们还会用它做其他事情。
我们还需要用之前创建的 sizes 对象,通过 setSize(...) 方法来更新渲染器的大小。
setSize(...) 方法会自动把我们 <canvas> 相应地调整大小:
// Canvas
const canvas = document.querySelector('canvas.webgl')
// ...
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas
})
renderer.setSize(sizes.width, sizes.height) 现在,你还看不到任何东西,但你的 canvas 就在那里,并且已经被相应调整大小了。如果你好奇,可以用开发者工具检查一下这个 <canvas>。
第一次渲染
是时候进行第一次渲染了。在渲染器上调用 render(...) 方法,并把 scene 和 camera 作为参数传进去:
renderer.render(scene, camera)
黑屏?我们的物体去哪了?
问题出在这:我们没有指定物体的位置,也没有指定相机的位置。两者都在默认位置,也就是场景中心,而我们不能从物体内部看到它(默认情况下)。
我们需要移动一些东西。
要做到这一点,每个物体上我们都可以访问多个属性,比如 position(位置)、rotation(旋转)和 scale(缩放)。现在,用 position 属性把相机向后移。
position 属性是一个对象,有三个相关属性:x、y 和 z。默认情况下,Three.js 把前后方向的轴视为 z。
要把相机向后移,我们需要给该属性提供一个正值。只要在创建 camera 变量之后、渲染之前,你可以在任何地方做这件事:
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
camera.position.z = 3
scene.add(camera)
恭喜,你应该能看到第一次渲染了。它看起来像一个正方形,那是因为相机恰好正对着立方体,你只能看到它的一面。
不用担心渲染的大小,我们以后会学习如何让 canvas 适配视口。
在接下来的课程中,你会学到更多关于 position、rotation 和 scale 属性的知识,如何修改它们,以及如何让场景动起来。
使用 starter 文件
虽然我们已经学会了如何从零开始,但每个练习都会附带一个 starter(起始)文件,这样你就不必每次都重复整个流程了。
而且,本课也确实可以找到 starter 文件。
后面大多数课程的工作方式都一样,我会解释如何使用这些文件,以便你边看课边完成练习。
Zip 文件
下载并解压 starter 文件(在播放器正下方)。
解压文件取决于你的操作系统以及安装了哪些应用。在 MacOS 上,你可以直接双击。在 Windows 上,你应该右键点击,选择类似 Extract…(解压)的选项,然后选择解压到的文件夹等选项。
依赖
你可能已经注意到没有 node_modules/ 文件夹。正如前面提到的,分享 Node.js 项目时,不要把 node_modules/ 一起分享,因为这个文件夹可能会变得非常大。
但我们确实需要这些依赖,而它们其实都列在 package.json 文件里。要安装这些列出的依赖,在项目文件夹中打开终端并运行 npm install:
稍等片刻,等依赖被拉取下来,你应该会看到 node_modules 文件夹被创建出来,旁边还有 package-lock.json。
运行服务器
现在,要运行服务器,在项目文件夹的终端中运行 npm run dev。
等一两秒钟,网站应该会在你的默认浏览器中打开。
如果页面没有自动打开,终端应该会显示两个 URL,看起来类似 http://localhost:5173/ 和 http://192.168.1.25:5173/。
在浏览器中试试它们:
这里的配置比我们之前从零搭建的稍微高级一些。
有一个 vite.config.js,里面我提供了一些特定设置。我们不会深入细节,因为我们已经花太多时间聊 Vite 了。不过,如果你好奇,可以在 Vite 文档找到所有你需要的信息。
故障排除
如果一切都正常,你可以跳过本节。
如果你一路上遇到了问题,接下来我会列举一些常见的故障以及如何修复。
终端所在文件夹
确保你的终端是在项目文件夹中打开的。
用 pwd 命令显示终端当前所在的文件夹。
用 cd (末尾带空格)命令,把文件夹拖进去,然后按 Enter 让终端进入那个文件夹。
路径过长
如果你的项目文件夹嵌套在别的文件夹里非常深,你可能会遇到路径太长,导致 Node.js 无法处理的情况。
把文件夹往上层移动,并记得让终端也跟着移动,然后再试一次。
带版本同步的文件夹
小心 OneDrive、Google Drive、Dropbox 这类工具,它们会把你的文件"保存"到云端。
它们可能会弄乱你的 Node.js 依赖。
尽量把项目移到这些"已保存"文件夹之外。
路径中的特殊字符
避免从电脑根目录直到项目之间出现特殊字符。
理想情况下,应该只使用小写字母、数字、短横线和下划线。
MacOS 和 Xcode
MacOS 可能会要求你安装"Xcode Command Line Tools"(Xcode 命令行工具)。
这通常是无害的提示,但有时它会阻止 Node.js 项目运行。
如果你想了解更多信息,可以看看这篇文章。简单来说,只需在终端中运行 xcode-select --install,然后按照提示操作即可。
权限
有时权限会出问题,导致 NPM 无法安装依赖。
如果你清楚自己在做什么,可以尝试修复权限。否则,删除文件夹,重新下载 starter,再按步骤操作一次。
漏洞警告
在安装依赖时,NPM 可能会警告你有"vulnerabilities"(漏洞)。这些大多数时候(如果不是总是的话)是无害的。我会定期更新课程依赖以避免这些警告,但你不必太担心。
不要试图用 NPM 建议的命令去修复它们,否则你最终得到的版本可能和我们课上用的不一样,从而引发 bug。
还是不行
如果你找不到问题所在,去课程的 Discord 服务器,把你掌握的任何信息都分享出来。我们会帮你解决。
关于 Vite 配置的更多内容
关于 Vite 配置,有几点你需要了解,以及一些提醒:
- 下载项目后,
npm install只需要运行一次。 - 每次想运行服务器、开始编码时,都需要运行
npm run dev。你的终端看起来可能像卡住了,但这完全正常,说明服务器正在运行。 - 你只需要探索
src/和static/这两个文件夹。 - 在
src/文件夹里,你可以找到常见的index.html、script.js和style.css文件。 - 你可以把静态文件(也叫"public files"公开文件)放在
static/文件夹里。这些文件会被当作位于根文件夹中来提供(无需写static/)。你可以通过在 URL 末尾加上/door.jpg来测试(http://localhost:5173/door.jpg)。以后我们会用它来加载纹理和模型。 - 当你保存这些文件中的任何一个时,页面会自动重新加载。
- 你可以通过输入那个看起来像
http://192.168.1.25:5173的相同 URL,从同一网络上的任何其他设备访问你的本地服务器。这在手机等其他设备上调试非常有用。如果不行,通常是因为防火墙。
结语
就是这样。我们已经准备好学习后面的许多课程,并使用各种练习了。
如果你对自己独立重做这些还不够自信,别担心,我们会在下一节课一起再做一遍。
记住,下面有些课程是用 Webpack 录制的,你可能会看到一些细微差别。那些差别你可以忽略。
我们现在做点 Three.js 怎么样?